package org.mysterylab.utopiaframework.commons.security.jcaptcha;

import java.awt.image.BufferedImage;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.mysterylab.utopiaframework.util.web.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import com.octo.captcha.service.CaptchaService;
import com.octo.captcha.service.CaptchaServiceException;

/**
 * 集成 JCaptcha 验证码的 Filter, 可通过配置实现与 Spring Security 的集成. 主要是登录表单处理.<br>
 * 在 web.xml 中可配置如下:
 * <pre>
 * {@code
 *  <filter>
 *      <filter-name>jcaptchaFilter</filter-name>
 *      <filter-class>org.mysterylab.utopiaframework.commons.security.jcaptcha.JCaptchaFilter</filter-class>
 *      <init-param>
 *          <param-name>failureUrl</param-name>
 *          <param-value>/security/login.do?error=2</param-value>
 *      </init-param>
 *      <init-param>
 *          <param-name>autoPassValue</param-name>
 *          <param-value>mysterylab</param-value>
 *      </init-param>
 *  </filter>
 *  <!-- jcaptcha 生成图片的 url -->
 *  <filter-mapping>
 *      <filter-name>jcaptchaFilter</filter-name>
 *      <url-pattern>/security/jcaptcha.jpg</url-pattern>
 *  </filter-mapping>
 *  <!-- jcaptcha 处理登录表单 url, 注意必须放在 springSecurityFilter 之前 -->
 *  <filter-mapping>
 *      <filter-name>jcaptchaFilter</filter-name>
 *      <url-pattern>/j_spring_security_check</url-pattern>
 *  </filter-mapping>
 * }
 * </pre>
 * 在 login.jsp 中可编写如下:
 * <pre>
 * {@code
 *  <form id="loginForm" action="/j_spring_security_check" method="post">
 *      <input type="text" name="j_username"
 *          <s:if test\"not empty param.error">value='<%=session.getAttribute(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_LAST_USERNAME_KEY)%>'</s:if> />
 *      <input type="password" name="j_password" />
 *      <input type="text" name="j_captcha" /><img id="captchaImg" src="/security/jcaptcha.jpg" />
 *      <a href="javascript:refreshCaptcha()">看不清楚点击换一张</a>
 *      <input type="checkbox" name="_spring_security_remember_me" />两周内记住我
 *      <script type="text/javascript">
 *          function refreshCaptcha() {
 *              $('#captchaImg').hide().attr('src','/security/jcaptcha.jpg?' + Math.floor(Math.random()*100)).fadeIn();
 *          }
 *      </script>
 *  </form>
 * }
 * </pre>
 * 
 * @author zhouych
 *
 */
public class JCaptchaFilter implements Filter {
	
	//-- web.xml 中的参数名定义 --//
	
	/**
	 * 登录表单中验证码 input 框中的名称, 默认为 "j_captcha"
	 */
	public static final String PARAM_CAPTCHA_PARAMTER_NAME = "captchaParamterName";
	
	/**
	 * captchaService 在 Spring Application 中的 bean id, 默认为 "captchaService"
	 */
	public static final String PARAM_CAPTCHA_SERVICE_ID = "captchaServiceId";
	
	/**
	 * 登录表单处理的 URL, 这里需要与 Spring Security中的配置一致, 默认为 "/j_spring_security_check"
	 */
	public static final String PARAM_FILTER_PROCESSES_URL = "filterProcessesUrl";
	
	/**
	 * 登录验证失败后跳转的 URL, 需要与 Spring Security 中的配置保持一致
	 */
	public static final String PARAM_FAILURE_URL = "failureUrl";
	
	/**
	 * 用于自动化功能测试的自动通过值, 主要是为了开发中的方便
	 */
	public static final String PARAM_AUTO_PASS_VALUE = "autoPassValue";
	
	//-- 定义默认值 --//
	
	/**
	 * 登录表单处理的 URL 的默认值
	 */
	public static final String DEFAULT_FILTER_PROCESSES_URL = "/j_spring_security_check";
	
	/**
	 * captchaService 在 Spring Application 中的 bean id 的默认值
	 */
	public static final String DEFAULT_CAPTCHA_SERVICE_ID = "captchaService";
	
	/**
	 * 登录表单中验证码 input 框中的名称的默认值
	 */
	public static final String DEFAULT_CAPTCHA_PARAMTER_NAME = "j_captcha";
	
	private static Logger logger = LoggerFactory.getLogger(JCaptchaFilter.class);
	
	/**
	 * 登录表单处理的 URL
	 */
	private String filterProcessesUrl = DEFAULT_FILTER_PROCESSES_URL;
	
	/**
	 * captchaService 在 Spring Application 中的 bean id
	 */
	private String captchaServiceId = DEFAULT_CAPTCHA_SERVICE_ID;
	
	/**
	 * 登录表单中验证码 input 框中的名称
	 */
	private String captchaParameterName = DEFAULT_CAPTCHA_PARAMTER_NAME;
	
	/**
	 * 登录验证失败后跳转的 URL
	 */
	private String failureUrl;
	
	/**
	 * 自动通过值
	 */
	private String autoPassValue;
	
	/**
	 * JCaptcha 的服务类
	 */
	private CaptchaService captchaService;
	
	@Override
	public void init(final FilterConfig config) throws ServletException {
		initParameters(config);
		initCaptchaService(config);
	}
	
	/**
	 * 初始化 web.xml 中定义的 filter 的 init-param.
	 * 
	 * @param config
	 */
	protected void initParameters(final FilterConfig config) {
		if (StringUtils.isBlank(config.getInitParameter(PARAM_FAILURE_URL))) {
			throw new IllegalArgumentException("failureUrl of captchaFilter can not be null");
		}
		failureUrl = config.getInitParameter(PARAM_FAILURE_URL);
		
		if (StringUtils.isNotBlank(config.getInitParameter(PARAM_FILTER_PROCESSES_URL))) {
			filterProcessesUrl = config.getInitParameter(PARAM_FILTER_PROCESSES_URL);
		}

		if (StringUtils.isNotBlank(config.getInitParameter(PARAM_CAPTCHA_SERVICE_ID))) {
			captchaServiceId = config.getInitParameter(PARAM_CAPTCHA_SERVICE_ID);
		}

		if (StringUtils.isNotBlank(config.getInitParameter(PARAM_CAPTCHA_PARAMTER_NAME))) {
			captchaParameterName = config.getInitParameter(PARAM_CAPTCHA_PARAMTER_NAME);
		}

		if (StringUtils.isNotBlank(config.getInitParameter(PARAM_AUTO_PASS_VALUE))) {
			autoPassValue = config.getInitParameter(PARAM_AUTO_PASS_VALUE);
		}
	}
	
	/**
	 * 从 Spring Application Context 中获取 CaptchaService 的实例.
	 * 
	 * @param config
	 */
	protected void initCaptchaService(final FilterConfig config) {
		ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(
				config.getServletContext());
		captchaService = (CaptchaService) context.getBean(captchaServiceId);
	}

	@Override
	public void destroy() {
		
	}

	@Override
	public void doFilter(final ServletRequest request, final ServletResponse response,
			final FilterChain filterChain) throws IOException, ServletException {
		
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		HttpServletResponse httpResponse = (HttpServletResponse) response;
		String servletPath = httpRequest.getServletPath();
		
		// 符合 filterProcessesUrl 为验证处理请求, 其余为生成验证图片请求
		if (StringUtils.startsWith(servletPath, filterProcessesUrl)) {
			// 是否验证通过
			boolean isValid = validateCaptcha(httpRequest);
			if (isValid) {
				filterChain.doFilter(httpRequest, httpResponse);
			} else {
				redirectFailureUrl(httpRequest, httpResponse);
			}
		} else {
			generateCaptcha(httpRequest, httpResponse);
		}
	}

	/**
	 * 验证验证码.
	 * 
	 * @param request
	 * @return
	 */
	protected boolean validateCaptcha(final HttpServletRequest request) {
		try {
			String captchaId = request.getSession().getId();
			String challengeResponse = request.getParameter(captchaParameterName);
			
			// 如果值属于自动通过值, 则放行
			if (StringUtils.isNotBlank(autoPassValue)
					&& autoPassValue.equals(challengeResponse)) {
				return true;
			}
			return captchaService.validateResponseForID(captchaId, challengeResponse);
		} catch (CaptchaServiceException e) {
			logger.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * 跳转到失败页面. 扩展子类的时候可以在 session 中放入 Spring Security 的 Exception.
	 * 
	 * @param request
	 * @param response
	 * @throws IOException
	 */
	protected void redirectFailureUrl(final HttpServletRequest request,
			final HttpServletResponse response) throws IOException {
		response.sendRedirect(request.getContextPath() + failureUrl);
	}
	
	/**
	 * 生成验证码的图片然后发给客户端.
	 * 
	 * @param request
	 * @param response
	 * @throws IOException
	 */
	protected void generateCaptcha(final HttpServletRequest request,
			final HttpServletResponse response) throws IOException {
		
		// 设置返回图片类型
		HttpUtil.setDisableCacheHeader(response);
		response.setContentType("image/jpeg");
		
		ServletOutputStream output = response.getOutputStream();
		try {
			String captchaId = request.getSession(true).getId();
			BufferedImage challenge = (BufferedImage) captchaService.getChallengeForID(
					captchaId, request.getLocale());
			ImageIO.write(challenge, "jpg", output);
			output.flush();
		} catch (CaptchaServiceException e) {
			logger.error(e.getMessage(), e);
		} finally {
			output.close();
		}
	}
}
